/**
 * This file is part of aion-emu <aion-emu.com>.
 *
 *  aion-emu is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  aion-emu is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with aion-emu. If not, see <http://www.gnu.org/licenses/>.
 */
package com.aionengine.gameserver.model.gameobjects;

import com.aionemu.commons.utils.internal.chmv8.PlatformDependent;
import com.aionengine.gameserver.ai2.AI2;
import com.aionengine.gameserver.ai2.AI2Engine;
import com.aionengine.gameserver.controllers.CreatureController;
import com.aionengine.gameserver.controllers.ObserveController;
import com.aionengine.gameserver.controllers.attack.AggroList;
import com.aionengine.gameserver.controllers.effect.EffectController;
import com.aionengine.gameserver.controllers.movement.MoveController;
import com.aionengine.gameserver.dataholders.DataManager;
import com.aionengine.gameserver.model.CreatureType;
import com.aionengine.gameserver.model.Race;
import com.aionengine.gameserver.model.TribeClass;
import com.aionengine.gameserver.model.gameobjects.player.Player;
import com.aionengine.gameserver.model.gameobjects.state.CreatureSeeState;
import com.aionengine.gameserver.model.gameobjects.state.CreatureState;
import com.aionengine.gameserver.model.gameobjects.state.CreatureVisualState;
import com.aionengine.gameserver.model.stats.container.CreatureGameStats;
import com.aionengine.gameserver.model.stats.container.CreatureLifeStats;
import com.aionengine.gameserver.model.templates.VisibleObjectTemplate;
import com.aionengine.gameserver.model.templates.item.ItemAttackType;
import com.aionengine.gameserver.model.templates.item.ItemTemplate;
import com.aionengine.gameserver.model.templates.spawns.SpawnTemplate;
import com.aionengine.gameserver.model.templates.zone.ZoneClassName;
import com.aionengine.gameserver.model.templates.zone.ZoneType;
import com.aionengine.gameserver.skillengine.effect.AbnormalState;
import com.aionengine.gameserver.skillengine.model.Skill;
import com.aionengine.gameserver.skillengine.model.SkillTemplate;
import com.aionengine.gameserver.taskmanager.tasks.PacketBroadcaster;
import com.aionengine.gameserver.taskmanager.tasks.PacketBroadcaster.BroadcastMode;
import com.aionengine.gameserver.world.MapRegion;
import com.aionengine.gameserver.world.WorldPosition;
import com.aionengine.gameserver.world.zone.ZoneInstance;
import com.aionengine.gameserver.world.zone.ZoneName;
import javolution.util.FastMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.Map;

/**
 * This class is representing movable objects, its base class for all in game objects that may move
 *
 * @author -Nemesiss-
 */
public abstract class Creature extends VisibleObject {

    private static final Logger log = LoggerFactory.getLogger(Creature.class);

    protected AI2 ai2;
    private boolean isDespawnDelayed = false;

    private CreatureLifeStats<? extends Creature> lifeStats;
    private CreatureGameStats<? extends Creature> gameStats;

    private EffectController effectController;
    protected MoveController moveController;

    private int state = CreatureState.ACTIVE.getId();
    private int visualState = CreatureVisualState.VISIBLE.getId();
    private int seeState = CreatureSeeState.NORMAL.getId();

    private Skill castingSkill;
    private Map<Integer, Long> skillCoolDowns;
    private Map<Integer, Long> skillCoolDownsBase;
    private ObserveController observeController;
    private TransformModel transformModel;

    private final AggroList aggroList;

    private byte adminFlags = 0;

    private Item usingItem;

    private final transient byte[] zoneTypes = new byte[ZoneType.values().length];

    private int skillNumber;
    private int attackedCount;
    private long spawnTime = System.currentTimeMillis();
    protected int type = CreatureType.NULL.getId();
    private TribeClass tribe = TribeClass.GENERAL;

    /**
     * @param objId
     * @param controller
     * @param spawnTemplate
     * @param objectTemplate
     * @param position
     */
    public Creature(int objId, CreatureController<? extends Creature> controller, SpawnTemplate spawnTemplate,
                    VisibleObjectTemplate objectTemplate, WorldPosition position) {
        super(objId, controller, spawnTemplate, objectTemplate, position);
        this.observeController = new ObserveController();
        this.setTransformModel(new TransformModel(this));
        if (spawnTemplate != null && spawnTemplate.getModel() != null) {
            if (spawnTemplate.getModel().getTribe() != null)
                getTransformModel().setTribe(spawnTemplate.getModel().getTribe(), true);
        }
        this.aggroList = createAggroList();
    }

    public MoveController getMoveController() {
        return this.moveController;
    }

    protected AggroList createAggroList() {
        return new AggroList(this);
    }

    /**
     * Return CreatureController of this Creature object.
     *
     * @return CreatureController.
     */
    @Override
    public CreatureController<? extends Creature> getController() {
        return (CreatureController<?>) super.getController();
    }

    /**
     * @return the lifeStats
     */
    public CreatureLifeStats<? extends Creature> getLifeStats() {
        return lifeStats;
    }

    /**
     * @param lifeStats the lifeStats to set
     */
    public void setLifeStats(CreatureLifeStats<? extends Creature> lifeStats) {
        this.lifeStats = lifeStats;
    }

    /**
     * @return the gameStats
     */
    public CreatureGameStats<? extends Creature> getGameStats() {
        return gameStats;
    }

    /**
     * @param gameStats the gameStats to set
     */
    public void setGameStats(CreatureGameStats<? extends Creature> gameStats) {
        this.gameStats = gameStats;
    }

    public abstract byte getLevel();

    /**
     * @return the effectController
     */
    public EffectController getEffectController() {
        return effectController;
    }

    /**
     * @param effectController the effectController to set
     */
    public void setEffectController(EffectController effectController) {
        this.effectController = effectController;
    }

    public AI2 getAi2() {
        return ai2 != null ? ai2 : AI2Engine.getInstance().setupAI("dummy", this);
    }

    public void setAi2(AI2 ai2) {
        this.ai2 = ai2;
    }

    public boolean isDeleteDelayed() {
        return isDespawnDelayed;
    }

    public void setDespawnDelayed(boolean delayed) {
        isDespawnDelayed = delayed;
    }

    public boolean isFlag() {
        return false;
    }

    /**
     * Is creature casting some skill
     *
     * @return
     */
    public boolean isCasting() {
        return castingSkill != null;
    }

    /**
     * Set current casting skill or null when skill ends
     *
     * @param castingSkill
     */
    public void setCasting(Skill castingSkill) {
        if (castingSkill != null)
            skillNumber++;
        this.castingSkill = castingSkill;
    }

    /**
     * Current casting skill id
     *
     * @return
     */
    public int getCastingSkillId() {
        return castingSkill != null ? castingSkill.getSkillTemplate().getSkillId() : 0;
    }

    /**
     * Current casting skill
     *
     * @return
     */
    public Skill getCastingSkill() {
        return castingSkill;
    }

    public int getSkillNumber() {
        return skillNumber;
    }

    public void setSkillNumber(int skillNumber) {
        this.skillNumber = skillNumber;
    }

    public int getAttackedCount() {
        return this.attackedCount;
    }

    public void incrementAttackedCount() {
        this.attackedCount++;
    }

    public void clearAttackedCount() {
        attackedCount = 0;
    }

    /**
     * Is using item
     *
     * @return
     */
    public boolean isUsingItem() {
        return usingItem != null;
    }

    /**
     * Set using item
     *
     * @param usingItem
     */
    public void setUsingItem(Item usingItem) {
        this.usingItem = usingItem;
    }

    /**
     * get Using ItemId
     *
     * @return
     */
    public int getUsingItemId() {
        return usingItem != null ? usingItem.getItemTemplate().getTemplateId() : 0;
    }

    /**
     * Using Item
     *
     * @return
     */
    public Item getUsingItem() {
        return usingItem;
    }

    /**
     * All abnormal effects are checked that disable movements
     *
     * @return
     */
    public boolean canPerformMove() {
        return !(getEffectController().isAbnormalState(AbnormalState.CANT_MOVE_STATE) || !isSpawned() || !canUseSkillInMove());
    }

    private boolean canUseSkillInMove() {
        if (castingSkill != null) {
            SkillTemplate st = DataManager.SKILL_DATA.getSkillTemplate(castingSkill.getSkillId());
            if (st.getStartconditions() != null && st.getMovedCondition() != null) {
                if (!st.getMovedCondition().isAllow())
                    return false;
            }
        }
        return true;
    }

    /**
     * All abnormal effects are checked that disable attack
     *
     * @return
     */
    public boolean canAttack() {
        return !(getEffectController().isAbnormalState(AbnormalState.CANT_ATTACK_STATE) || isCasting() || isInState(CreatureState.RESTING) || isInState(CreatureState.PRIVATE_SHOP));
    }

    /**
     * @return state
     */
    public int getState() {
        return state;
    }

    /**
     * @param state the state to set
     */
    public void setState(CreatureState state) {
        this.state |= state.getId();
    }

    /**
     * @param state taken usually from templates
     */
    public void setState(int state) {
        this.state = state;
    }

    public void unsetState(CreatureState state) {
        this.state &= ~state.getId();
    }

    public boolean isInState(CreatureState state) {
        int isState = this.state & state.getId();

        if (isState == state.getId())
            return true;

        return false;
    }

    /**
     * @return visualState
     */
    public int getVisualState() {
        return visualState;
    }

    /**
     * @param visualState the visualState to set
     */
    public void setVisualState(CreatureVisualState visualState) {
        this.visualState |= visualState.getId();
    }

    public void unsetVisualState(CreatureVisualState visualState) {
        this.visualState &= ~visualState.getId();
    }

    public boolean isInVisualState(CreatureVisualState visualState) {
        int isVisualState = this.visualState & visualState.getId();

        if (isVisualState == visualState.getId())
            return true;

        return false;
    }

    /**
     * @return seeState
     */
    public int getSeeState() {
        return seeState;
    }

    /**
     * @param seeState the seeState to set
     */
    public void setSeeState(CreatureSeeState seeState) {
        this.seeState |= seeState.getId();
    }

    public void unsetSeeState(CreatureSeeState seeState) {
        this.seeState &= ~seeState.getId();
    }

    public boolean isInSeeState(CreatureSeeState seeState) {
        int isSeeState = this.seeState & seeState.getId();

        if (isSeeState == seeState.getId())
            return true;

        return false;
    }

    /**
     * @return the transformModel
     */
    public TransformModel getTransformModel() {
        return transformModel;
    }

    /**
     * @param transformModel the transformedModel to set
     */
    public final void setTransformModel(TransformModel model) {
        this.transformModel = model;
    }

    /**
     * @return the aggroList
     */
    public final AggroList getAggroList() {
        return aggroList;
    }

    /**
     * PacketBroadcasterMask
     */
    private volatile byte packetBroadcastMask;

    /**
     * This is adding broadcast to player.
     */
    public final void addPacketBroadcastMask(BroadcastMode mode) {
        packetBroadcastMask |= mode.mask();

        PacketBroadcaster.getInstance().add(this);

        // Debug
        if (log.isDebugEnabled())
            log.debug("PacketBroadcaster: Packet " + mode.name() + " added to player " + this.getName());
    }

    /**
     * This is removing broadcast from player.
     */
    public final void removePacketBroadcastMask(BroadcastMode mode) {
        packetBroadcastMask &= ~mode.mask();

        // Debug
        if (log.isDebugEnabled())
            log.debug("PacketBroadcaster: Packet " + mode.name() + " removed from player " + this.getName()); // fix
        // ClassCastException
    }

    /**
     * Broadcast getter.
     */
    public final byte getPacketBroadcastMask() {
        return packetBroadcastMask;
    }

    /**
     * @return the observeController
     */
    public ObserveController getObserveController() {
        return observeController;
    }

    /**
     * Double dispatch like method
     *
     * @param creature
     * @return
     */
    public boolean isEnemy(Creature creature) {
        return creature.isEnemyFrom(this);
    }

    /**
     * @param creature
     */
    public boolean isEnemyFrom(Creature creature) {
        return false;
    }

    /**
     * @param player
     * @return
     */
    public boolean isEnemyFrom(Player player) {
        return false;
    }

    /**
     * @param npc
     * @return
     */
    public boolean isEnemyFrom(Npc npc) {
        return false;
    }

    public TribeClass getTribe() {
        return tribe;
    }

    public void setTribe(TribeClass tribe) {
        this.tribe = tribe;
    }

    public TribeClass getBaseTribe() {
        return TribeClass.GENERAL;
    }

    @Override
    public boolean canSee(Creature creature) {
        if (creature == null)
            return false;

        return creature.getVisualState() <= getSeeState();
    }

    public boolean isSeeObject(VisibleObject object) {
        return getKnownList().getVisibleObjects().containsKey(object.getObjectId());
    }

    public boolean isSeePlayer(Player player) {
        return getKnownList().getVisiblePlayers().containsKey(player.getObjectId());
    }

    /**
     * @return NpcObjectType.NORMAL
     */
    public NpcObjectType getNpcObjectType() {
        return NpcObjectType.NORMAL;
    }

    /**
     * For summons and different kind of servants<br>
     * it will return currently acting player.<br>
     * This method is used for duel and enemy relations,<br>
     * rewards<br>
     *
     * @return Master of this creature or self
     */
    public Creature getMaster() {
        return this;
    }

    /**
     * For summons it will return summon object and for <br>
     * servants - player object.<br>
     * Used to find attackable target for npcs.<br>
     *
     * @return acting master - player in case of servants
     */
    public Creature getActingCreature() {
        return this;
    }

    /**
     * @param cooldownId
     * @return
     */
    public boolean isSkillDisabled(SkillTemplate template) {

        if (skillCoolDowns == null)
            return false;

        int cooldownId = template.getCooldownId();
        Long coolDown = skillCoolDowns.get(cooldownId);
        if (coolDown == null) {
            return false;
        }

        if (coolDown < System.currentTimeMillis()) {
            removeSkillCoolDown(cooldownId);
            return false;
        }

		/*
         * Some shared cooldown skills have indipendent and different cooldown they must not be blocked
		 */
        if (skillCoolDownsBase != null && skillCoolDownsBase.get(cooldownId) != null) {
            if ((template.getDuration() + template.getCooldown() * 100 + skillCoolDownsBase.get(cooldownId)) < System.currentTimeMillis())
                return false;
        }

        return true;
    }

    /**
     * @param cooldownId
     * @return
     */
    public long getSkillCoolDown(int cooldownId) {
        if (skillCoolDowns == null || !skillCoolDowns.containsKey(cooldownId))
            return 0;

        return skillCoolDowns.get(cooldownId);
    }

    /**
     * @param cooldownId
     * @param time
     */
    public void setSkillCoolDown(int cooldownId, long time) {

        if (cooldownId == 0) {
            return;
        }

        if (skillCoolDowns == null)
            skillCoolDowns = PlatformDependent.newConcurrentHashMap();
        skillCoolDowns.put(cooldownId, time);
    }

    /**
     * @return the skillCoolDowns
     */
    public FastMap<Integer, Long> getSkillCoolDowns() {
        FastMap<Integer, Long> tmp = new FastMap<Integer, Long>();
        if (skillCoolDowns == null)
            return tmp;
        tmp.putAll(skillCoolDowns);
        return tmp;
    }

    /**
     * @param cooldownId
     */
    public void removeSkillCoolDown(int cooldownId) {
        if (skillCoolDowns == null)
            return;
        skillCoolDowns.remove(cooldownId);
        if (skillCoolDownsBase != null)
            skillCoolDownsBase.remove(cooldownId);
    }

    /**
     * This function saves the currentMillis of skill that generated the cooldown of an entire cooldownGroup
     *
     * @param cooldownId
     * @param baseTime
     */
    public void setSkillCoolDownBase(int cooldownId, long baseTime) {

        if (cooldownId == 0) {
            return;
        }

        if (skillCoolDownsBase == null)
            skillCoolDownsBase = PlatformDependent.newConcurrentHashMap();
        skillCoolDownsBase.put(cooldownId, baseTime);
    }

    /**
     * @return isAdminNeutral value
     */
    public int getAdminNeutral() {
        return adminFlags >> 4;
    }

    /**
     * @param newValue
     */
    public void setAdminNeutral(int newValue) {
        adminFlags = (byte) ((adminFlags & 0xF) | (newValue & 0xF) << 4);
    }

    /**
     * @return isAdminEnmity value
     */
    public int getAdminEnmity() {
        return adminFlags & 0xF;
    }

    /**
     * @param newValue
     */
    public void setAdminEnmity(int newValue) {
        adminFlags = (byte) ((adminFlags & 0xF0) | (newValue & 0xF));
    }

    public float getCollision() {
        return getObjectTemplate().getBoundRadius().getCollision();
    }

    /**
     * @return
     */
    public boolean isAttackableNpc() {
        return false;
    }

    public ItemAttackType getAttackType() {
        return ItemAttackType.PHYSICAL;
    }

    /**
     * Creature is flying (FLY or GLIDE states)
     */
    public boolean isFlying() {
        return (isInState(CreatureState.FLYING) && !isInState(CreatureState.RESTING)) || isInState(CreatureState.GLIDING);
    }

    public boolean isInFlyingState() {
        return isInState(CreatureState.FLYING) && !isInState(CreatureState.RESTING);
    }

    public byte isPlayer() {
        return 0;
    }

    public boolean isPvpTarget(Creature creature) {
        return getActingCreature() instanceof Player && creature.getActingCreature() instanceof Player;
    }

    public void revalidateZones() {
        MapRegion mapRegion = this.getPosition().getMapRegion();
        if (mapRegion != null)
            mapRegion.revalidateZones(this);
    }

    public boolean isInsideZone(ZoneName zoneName) {
        if (!isSpawned())
            return false;
        return getPosition().getMapRegion().isInsideZone(zoneName, this);
    }

    public boolean isInsideItemUseZone(ZoneName zoneName) {
        if (!isSpawned())
            return false;
        return getPosition().getMapRegion().isInsideItemUseZone(zoneName, this);
    }

    public boolean isInsideWeatherZone(int weatherZoneId) {
        if (getActiveRegion() == null)
            return false;
        List<ZoneInstance> zones = getActiveRegion().getZones(this);
        for (ZoneInstance regionZone : zones) {
            if (regionZone.getZoneTemplate().getZoneType() == ZoneClassName.WEATHER) {
                if (!regionZone.getAreaTemplate().isInside3D(getPosition().getX(), getPosition().getY(), getPosition().getZ()))
                    continue;
                if (DataManager.ZONE_DATA.getWeatherZoneId(regionZone.getZoneTemplate()) == weatherZoneId)
                    return true;
            }
        }
        return false;
    }

    public void setInsideZoneType(ZoneType zoneType) {
        byte current = zoneTypes[zoneType.getValue()];
        zoneTypes[zoneType.getValue()] = (byte) (current + 1);
    }

    public void unsetInsideZoneType(ZoneType zoneType) {
        byte current = zoneTypes[zoneType.getValue()];
        zoneTypes[zoneType.getValue()] = (byte) (current - 1);
    }

    public boolean isInsideZoneType(ZoneType zoneType) {
        return zoneTypes[zoneType.getValue()] > 0;
    }

    public Race getRace() {
        return Race.NONE;
    }

    public int getType(Creature creature) {
        return type;
    }

    public int getSkillCooldown(SkillTemplate template) {
        return template.getCooldown();
    }

    public int getItemCooldown(ItemTemplate template) {
        return template.getUseLimits().getDelayTime();
    }

    public boolean isNewSpawn() {
        return System.currentTimeMillis() - spawnTime < 1500;
    }
}
